// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file provides common functionality for different configurations of test
// extensions using the chrome.certificateProvider API.

'use strict';

const assertEq = chrome.test.assertEq;
const assertTrue = chrome.test.assertTrue;
const assertLastError = chrome.test.assertLastError;
const callbackPass = chrome.test.callbackPass;

// Make sure all unhandled errors result in the test failure.
chrome.test.setExceptionHandler(function(message, exc) {
  throw exc;
});
window.addEventListener('error', function(errorEvent) {
  chrome.test.fail(`Unhandled exception: ${errorEvent.error}`);
});

// X.509 certificate in DER encoding issued by 'root.pem' which is set to be
// trusted by the test setup.
// Read from 'l1_leaf.der', generated by create_test_certs.sh .
let l1LeafCert = null;
const INVALID_CERT = new Uint8Array([1, 2, 3, 4, 5]);
// These variables are manipulated by the C++ side in some test cases:
let supportedAlgorithms = ['RSASSA_PKCS1_v1_5_SHA1'];
let supportedLegacyHashes = ['SHA1'];

function getInvalidClientCertificateInfos() {
  const badDer = {
    certificateChain: [INVALID_CERT.buffer],
    supportedAlgorithms: ['RSASSA_PKCS1_v1_5_SHA256']
  };
  const emptyChain = {
    certificateChain: [],
    supportedAlgorithms: ['RSASSA_PKCS1_v1_5_SHA256']
  };
  const noAlgorithms = {
    certificateChain: [l1LeafCert.buffer],
    supportedAlgorithms: []
  };
  return [
    badDer,
    emptyChain,
    noAlgorithms,
  ];
}

function getInvalidLegacyCertificateInfos() {
  const badDer = {
    certificate: INVALID_CERT.buffer,
    supportedHashes: ['SHA256']
  };
  const noHashes = {certificate: l1LeafCert.buffer, supportedHashes: []};
  return [
    badDer,
    noHashes,
  ];
}

function registerAsCertificateProvider() {
  function reportCertificates(request) {
    assertTrue(Number.isInteger(request.certificatesRequestId));
    const validCert = {
      certificateChain: [l1LeafCert.buffer],
      supportedAlgorithms: supportedAlgorithms
    };
    chrome.certificateProvider.setCertificates(
        {
          certificatesRequestId: request.certificatesRequestId,
          clientCertificates: [validCert, ...getInvalidClientCertificateInfos()]
        },
        () => {
          chrome.test.succeed();
        });
  }

  chrome.certificateProvider.onCertificatesUpdateRequested.addListener(
      reportCertificates);
}

function registerAsLegacyCertificateProvider() {
  function checkResult(rejectedCerts) {
    assertEq(2, rejectedCerts.length);
    const rejectedCertsBytes = rejectedCerts.map(
        arrayBuffer => JSON.stringify(new Uint8Array(arrayBuffer)));
    assertTrue(rejectedCertsBytes.includes(JSON.stringify(INVALID_CERT)));
    assertTrue(rejectedCertsBytes.includes(JSON.stringify(l1LeafCert)));
  }

  function reportCertificates(reportCallback) {
    const validCertInfo = {
      certificate: l1LeafCert.buffer,
      supportedHashes: supportedLegacyHashes
    };
    reportCallback(
        [validCertInfo, ...getInvalidLegacyCertificateInfos()],
        callbackPass(checkResult));
  }

  chrome.certificateProvider.onCertificatesRequested.addListener(
      callbackPass(reportCertificates));
}

// Use setCertificates to let the extension proactively provide certificates.
// This can be combined with registerAsCertificateProvider(), but can also be
// used on its own.
function setCertificates() {
  const validCert = {
    certificateChain: [l1LeafCert.buffer],
    supportedAlgorithms: supportedAlgorithms
  };
  chrome.certificateProvider.setCertificates(
      {clientCertificates: [validCert, ...getInvalidClientCertificateInfos()]},
      () => {
        const success = !chrome.runtime.lastError;
        domAutomationController.send(success);
      });
}

// Similar to `setCertificates()`, but only provides invalid certificates.
function setInvalidCertificates() {
  chrome.certificateProvider.setCertificates(
      {clientCertificates: getInvalidClientCertificateInfos()}, () => {
        const success = !chrome.runtime.lastError;
        domAutomationController.send(success);
      });
}

// Indicates that there are no certificates available.
function unsetCertificates() {
  chrome.certificateProvider.setCertificates({clientCertificates: []}, () => {
    const success = !chrome.runtime.lastError;
    domAutomationController.send(success);
  });
}

let signatureRequestAlgorithm;
let signatureRequestData;
let signatureCallback;

function registerForSignatureRequests() {
  chrome.certificateProvider.onSignatureRequested.addListener(function(
      request) {
    assertTrue(Number.isInteger(request.signRequestId));
    assertEq(l1LeafCert.buffer, request.certificate);
    // The sign request must refer to the algorithm that was declared to be
    // supported.
    assertTrue(supportedAlgorithms.includes(request.algorithm));
    signatureCallback = (signature) => {
      // First, simulate malformed call parameters, with neither the signature
      // nor the error being set. This call should be rejected by the API.
      chrome.certificateProvider.reportSignature(
          {signRequestId: request.signRequestId}, () => {
            assertLastError('Neither the result nor an error supplied.');
          });
      // Then report the signature correctly.
      chrome.certificateProvider.reportSignature(
          {signRequestId: request.signRequestId, signature: signature});
    };
    signatureRequestAlgorithm = request.algorithm;
    signatureRequestData = request.input;
    chrome.test.sendMessage('signature request received');
  });
}

function registerForLegacySignatureRequests() {
  chrome.certificateProvider.onSignDigestRequested.addListener(function(
      request, callback) {
    assertEq(l1LeafCert.buffer, request.certificate);
    // The sign request must refer to the hash that was declared to be
    // supported.
    assertTrue(supportedLegacyHashes.includes(request.hash));
    signatureCallback = callback;
    signatureRequestAlgorithm = request.hash;
    signatureRequestData = request.digest;
    chrome.test.sendMessage('signature request received');
  });
}

function replyWithSignature(signature) {
  signatureCallback(signature.buffer);
}

function replyWithSignatureSecondTime() {
  const signature = new Uint8Array([1, 2, 3]);
  try {
    signatureCallback(signature.buffer);
  } catch (e) {
    domAutomationController.send(false);
    return false;
  }
  domAutomationController.send(true);
  return true;
}

// initialize is called from the cpp test.
// |cert| is the certificate data in an Uint8Array.
function initialize(cert) {
  l1LeafCert = cert;
}
